/* SPDX-License-Identifier: Apache-2.0 OR MIT
 *
 * SPDX-FileCopyrightText: Copyright 2021 Micron Technology, Inc.
 */

#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <bsd/string.h>
#include <cjson/cJSON.h>

#include <hse/config/params.h>
#include <hse/ikvdb/hse_gparams.h>
#include <hse/ikvdb/limits.h>
#include <hse/logging/logging.h>
#include <hse/util/compiler.h>
#include <hse/util/perfc.h>
#include <hse/util/vlb.h>

struct hse_gparams hse_gparams;

static bool HSE_NONNULL(1, 2, 3)
logging_destination_converter(
    const struct param_spec * const ps,
    const cJSON * const node,
    void * const data)
{
    const char *setting;
    enum log_destination log_dest;

    INVARIANT(ps);
    INVARIANT(node);
    INVARIANT(data);

    if (!cJSON_IsString(node))
        return false;

    setting = cJSON_GetStringValue(node);
    if (strcmp(setting, "stdout") == 0) {
        log_dest = LOG_DEST_STDOUT;
    } else if (strcmp(setting, "stderr") == 0) {
        log_dest = LOG_DEST_STDERR;
    } else if (strcmp(setting, "file") == 0) {
        log_dest = LOG_DEST_FILE;
    } else if (strcmp(setting, "syslog") == 0) {
        log_dest = LOG_DEST_SYSLOG;
    } else {
        log_err(
            "Invalid logging.destination value: %s, must be one of stdout, stderr, file, or syslog",
            setting);
        return false;
    }

    *(enum log_destination *)data = log_dest;

    return true;
}

static merr_t
logging_destination_stringify(
    const struct param_spec * const ps,
    const void * const value,
    char * const buf,
    const size_t buf_sz,
    size_t * const needed_sz)
{
    static const char *values[] = { "stdout", "stderr", "file", "syslog" };

    const int n = snprintf(buf, buf_sz, "\"%s\"", values[*(enum log_destination *)value]);
    if (n < 0)
        return merr(EBADMSG);

    if (needed_sz)
        *needed_sz = n;

    return 0;
}

static cJSON *
logging_destination_jsonify(const struct param_spec * const ps, const void * const value)
{
    assert(ps);
    assert(value);

    switch (*(enum log_destination *)value) {
    case LOG_DEST_STDOUT:
        return cJSON_CreateString("stdout");
    case LOG_DEST_STDERR:
        return cJSON_CreateString("stderr");
    case LOG_DEST_FILE:
        return cJSON_CreateString("file");
    case LOG_DEST_SYSLOG:
        return cJSON_CreateString("syslog");
    default:
        abort();
    }
}

static void
rest_socket_path_default(const struct param_spec *ps, void *value)
{
    const char *dir;
    HSE_MAYBE_UNUSED int n;

    INVARIANT(ps);
    INVARIANT(value);

    /* All paths set in these environment variables must be absolute. If an
     * implementation encounters a relative path in any of these variables it
     * should consider the path invalid and ignore it.
     *
     * https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
     */
    dir = getenv("XDG_RUNTIME_DIR");
    if (!dir || *dir != '/')
        dir = "/tmp";

    n = snprintf(
        value, sizeof(hse_gparams.gp_rest.socket_path), "%s%shse-%d.sock", dir,
        dir[strlen(dir) - 1] == '/' ? "" : "/", getpid());
    assert(n < sizeof(hse_gparams.gp_rest.socket_path) && n > 0);
}

static const struct param_spec pspecs[] = {
    {
        .ps_name = "logging.enabled",
        .ps_description = "Whether logging is enabled",
        .ps_flags = 0,
        .ps_type = PARAM_TYPE_BOOL,
        .ps_offset = offsetof(struct hse_gparams, gp_logging.lp_enabled),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_logging.lp_enabled),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_bool = true,
        },
    },
    {
        .ps_name = "logging.destination",
        .ps_description = "Where log messages should be written to",
        .ps_flags = 0,
        .ps_type = PARAM_TYPE_ENUM,
        .ps_offset = offsetof(struct hse_gparams, gp_logging.lp_destination),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_logging.lp_destination),
        .ps_convert = logging_destination_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = logging_destination_stringify,
        .ps_jsonify = logging_destination_jsonify,
        .ps_default_value = {
            .as_enum = LOG_DEST_SYSLOG,
        },
        .ps_bounds = {
            .as_enum = {
                .ps_min = LOG_DEST_MIN,
                .ps_max = LOG_DEST_MAX,
            }
        },
    },
    {
        .ps_name = "logging.path",
        .ps_description = "Name of log file when destination == file",
        .ps_flags = 0,
        .ps_type = PARAM_TYPE_STRING,
        .ps_offset = offsetof(struct hse_gparams, gp_logging.lp_path),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_logging.lp_path),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_string = "hse.log",
        },
        .ps_bounds = {
            .as_string = {
                .ps_max_len = PARAM_SZ(struct hse_gparams, gp_logging.lp_path),
            },
        },
    },
    {
        .ps_name = "logging.level",
        .ps_description = "Maximum log level which will be written",
        .ps_flags = 0,
        .ps_type = PARAM_TYPE_INT,
        .ps_offset = offsetof(struct hse_gparams, gp_logging.lp_level),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_logging.lp_level),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_scalar = LOG_DEFAULT,
        },
        .ps_bounds = {
            .as_scalar = {
                .ps_min = LOG_EMERG,
                .ps_max = LOG_DEBUG,
            }
        }
    },
    {
        .ps_name = "logging.squelch_ns",
        .ps_description = "drop messages repeated within nsec window",
        .ps_flags = PARAM_EXPERIMENTAL,
        .ps_type = PARAM_TYPE_U64,
        .ps_offset = offsetof(struct hse_gparams, gp_logging.lp_squelch_ns),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_logging.lp_squelch_ns),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_uscalar = LOG_SQUELCH_NS_DEFAULT,
        },
        .ps_bounds = {
            .as_uscalar = {
                .ps_min = 0,
                .ps_max = UINT64_MAX,
            },
        },
    },
    {
        .ps_name = "vlb_cache_sz",
        .ps_description = "size of vlb cache (bytes)",
        .ps_flags = PARAM_EXPERIMENTAL,
        .ps_type = PARAM_TYPE_U64,
        .ps_offset = offsetof(struct hse_gparams, gp_vlb_cache_sz),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_vlb_cache_sz),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_uscalar = HSE_VLB_CACHESZ_DFLT,
        },
        .ps_bounds = {
            .as_uscalar = {
                .ps_min = HSE_VLB_CACHESZ_MIN,
                .ps_max = HSE_VLB_CACHESZ_MAX,
            },
        },
    },
    {
        .ps_name = "c0kvs_ccache_sz_max",
        .ps_description = "max size of c0kvs cheap cache (bytes)",
        .ps_flags = PARAM_EXPERIMENTAL,
        .ps_type = PARAM_TYPE_U64,
        .ps_offset = offsetof(struct hse_gparams, gp_c0kvs_ccache_sz_max),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_c0kvs_ccache_sz_max),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_uscalar = HSE_C0_CCACHE_SZ_DFLT,
        },
        .ps_bounds = {
            .as_uscalar = {
                .ps_min = 0,
                .ps_max = HSE_C0_CCACHE_SZ_MAX,
            },
        },
    },
    {
        .ps_name = "c0kvs_ccache_sz",
        .ps_description = "size of c0kvs cheap cache (bytes)",
        .ps_flags = PARAM_EXPERIMENTAL,
        .ps_type = PARAM_TYPE_U64,
        .ps_offset = offsetof(struct hse_gparams, gp_c0kvs_ccache_sz),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_c0kvs_ccache_sz),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_uscalar = HSE_C0_CCACHE_SZ_DFLT,
        },
        .ps_bounds = {
            .as_uscalar = {
                .ps_min = 0,
                .ps_max = HSE_C0_CCACHE_SZ_MAX,
            },
        },
    },
    {
        .ps_name = "c0kvs_cheap_sz",
        .ps_description = "set c0kvs cheap size (bytes)",
        .ps_flags = PARAM_EXPERIMENTAL,
        .ps_type = PARAM_TYPE_U64,
        .ps_offset = offsetof(struct hse_gparams, gp_c0kvs_cheap_sz),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_c0kvs_cheap_sz),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_uscalar = HSE_C0_CHEAP_SZ_DFLT,
        },
        .ps_bounds = {
            .as_uscalar = {
                .ps_min = HSE_C0_CHEAP_SZ_MIN,
                .ps_max = HSE_C0_CHEAP_SZ_MAX,
            },
        },
    },
    {
        .ps_name = "workqueue_tcdelay",
        .ps_description = "set workqueue thread-create delay (milliseconds)",
        .ps_flags = PARAM_EXPERIMENTAL,
        .ps_type = PARAM_TYPE_U32,
        .ps_offset = offsetof(struct hse_gparams, gp_workqueue_tcdelay),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_workqueue_tcdelay),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_uscalar = 1000,
        },
        .ps_bounds = {
            .as_uscalar = {
                .ps_min = 0,
                .ps_max = UINT32_MAX,
            },
        },
    },
    {
        .ps_name = "workqueue_idle_ttl",
        .ps_description = "set workqueue idle thread time-to-live (seconds)",
        .ps_flags = PARAM_EXPERIMENTAL,
        .ps_type = PARAM_TYPE_U32,
        .ps_offset = offsetof(struct hse_gparams, gp_workqueue_idle_ttl),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_workqueue_idle_ttl),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_uscalar = 300,
        },
        .ps_bounds = {
            .as_uscalar = {
                .ps_min = 0,
                .ps_max = UINT32_MAX,
            },
        },
    },
    {
        .ps_name = "perfc.level",
        .ps_description = "set kvs perf counter enagagement level (min:0 default:2 max:9)",
        .ps_flags = PARAM_EXPERIMENTAL,
        .ps_type = PARAM_TYPE_U8,
        .ps_offset = offsetof(struct hse_gparams, gp_perfc_level),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_perfc_level),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_uscalar = PERFC_LEVEL_DEFAULT,
        },
        .ps_bounds = {
            .as_uscalar = {
                .ps_min = PERFC_LEVEL_MIN,
                .ps_max = PERFC_LEVEL_MAX,
            },
        },
    },
    {
        .ps_name = "rest.enabled",
        .ps_description = "Enable the REST server",
        .ps_flags = 0,
        .ps_type = PARAM_TYPE_BOOL,
        .ps_offset = offsetof(struct hse_gparams, gp_rest.enabled),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_rest.enabled),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_bool = true,
        }
    },
    {
        .ps_name = "rest.socket_path",
        .ps_description = "UNIX socket path to start REST server on",
        .ps_flags = PARAM_DEFAULT_BUILDER,
        .ps_type = PARAM_TYPE_STRING,
        .ps_offset = offsetof(struct hse_gparams, gp_rest.socket_path),
        .ps_size = PARAM_SZ(struct hse_gparams, gp_rest.socket_path),
        .ps_convert = param_default_converter,
        .ps_validate = param_default_validator,
        .ps_stringify = param_default_stringify,
        .ps_jsonify = param_default_jsonify,
        .ps_default_value = {
            .as_builder = rest_socket_path_default,
        },
        .ps_bounds = {
            .as_string = {
                .ps_max_len = PARAM_SZ(struct hse_gparams, gp_rest.socket_path),
            },
        },
    },
};

const struct param_spec *
hse_gparams_pspecs_get(size_t *pspecs_sz)
{
    if (pspecs_sz)
        *pspecs_sz = NELEM(pspecs);
    return pspecs;
}

struct hse_gparams
hse_gparams_defaults()
{
    struct hse_gparams params;

    params_from_defaults(&params, NELEM(pspecs), pspecs);

    return params;
}

merr_t
hse_gparams_get(
    const struct hse_gparams * const params,
    const char * const param,
    char * const buf,
    const size_t buf_sz,
    size_t * const needed_sz)
{
    return params_get(params, NELEM(pspecs), pspecs, param, buf, buf_sz, needed_sz);
}

merr_t
hse_gparams_set(
    struct hse_gparams * const params,
    const char * const param,
    const char * const value)
{
    if (!params || !param || !value)
        return merr(EINVAL);

    return params_set(params, sizeof(*params), NELEM(pspecs), pspecs, param, value);
}

merr_t
hse_gparams_from_config(struct hse_gparams *params, cJSON *config)
{
    if (!params || !cJSON_IsObject(config))
        return merr(EINVAL);

    return params_from_config(params, NELEM(pspecs), pspecs, 0, NULL, 1, config);
}

merr_t
hse_gparams_from_paramv(
    struct hse_gparams * const params,
    const size_t paramc,
    const char * const * const paramv)
{
    return params_from_paramv(params, paramc, paramv, NELEM(pspecs), pspecs);
}

cJSON *
hse_gparams_to_json(const struct hse_gparams * const params)
{
    if (!params)
        return NULL;

    return params_to_json(params, NELEM(pspecs), pspecs);
}
